package com.googlecode.stateless4j;

import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.commons.lang.StringUtils;
import com.googlecode.stateless4j.delegates.Func2;
import com.googlecode.stateless4j.delegates.UnhandledTransitionRunnable;
import com.googlecode.stateless4j.transitions.Transition;
import com.googlecode.stateless4j.transitions.TransitioningTriggerBehaviour;
import com.googlecode.stateless4j.triggers.TriggerBehaviour;
import com.googlecode.stateless4j.triggers.TriggerWithParameters;
import com.googlecode.stateless4j.triggers.TriggerWithParameters1;
import com.googlecode.stateless4j.triggers.TriggerWithParameters2;
import com.googlecode.stateless4j.triggers.TriggerWithParameters3;
import com.googlecode.stateless4j.validation.CannotReconfigureParametersException;
import com.googlecode.stateless4j.validation.ArgumentValidator;
import com.googlecode.stateless4j.validation.NoTransitionsPermittedException;
import com.googlecode.stateless4j.validation.UnhandledTriggerException;

/// <summary>
/// Models behaviour as transitions between a finite set of states.
/// </summary>
/// <typeparam name="TState">The type used to represent the states.</typeparam>
/// <typeparam name="TTrigger">The type used to represent the triggers that cause state transitions.</typeparam>
public class StateMachine<TState, TTrigger>
{
    private final Map<TState, StateRepresentation<TState, TTrigger>> stateConfiguration = new HashMap<TState, StateRepresentation<TState, TTrigger>>();
    private final Map<TTrigger, TriggerWithParameters<TState, TTrigger>> triggerConfiguration = new HashMap<TTrigger, TriggerWithParameters<TState, TTrigger>>();

    private final StateReference<TState, TTrigger> reference = new StateReference<TState, TTrigger>();

    private Runnable unhandledTriggerRunnable = null;

    /// <summary>
    /// Construct a state machine.
    /// </summary>
    /// <param name="initialState">The initial state.</param>
    public StateMachine(TState initialState)
    {
        reference.setState(initialState);
    }

    /// <summary>
    /// The current state.
    /// </summary>
    public TState getState()
    {
        return reference.getState();
    }

    private void setState(TState value)
    {
        reference.setState(value);
    }

    /// <summary>
    /// The currently-permissible trigger values.
    /// </summary>
    public List<TTrigger> getPermittedTriggers()
    {
        return getCurrentRepresentation().getPermittedTriggers();
    }

    public StateRepresentation<TState, TTrigger> getCurrentRepresentation()
    {
        return getRepresentation(getState());
    }

    public StateRepresentation<TState, TTrigger> getRepresentation(TState state)
    {

        if (!stateConfiguration.containsKey(state))
        {
            StateRepresentation<TState, TTrigger> result = new StateRepresentation<TState, TTrigger>(state);
            stateConfiguration.put(state, result);
        }

        return stateConfiguration.get(state);
    }

    /// <summary>
    /// Begin configuration of the entry/exit actions and allowed transitions
    /// when the state machine is in a particular state.
    /// </summary>
    /// <param name="state">The state to configure.</param>
    /// <returns>A configuration object through which the state can be configured.</returns>
    public StateConfiguration<TState, TTrigger> configure(TState state)
    {
        return new StateConfiguration<TState, TTrigger>(getRepresentation(state),
            new Func2<TState, StateRepresentation<TState, TTrigger>>()
            {

                public StateRepresentation<TState, TTrigger> call(TState arg0)
                {
                    return getRepresentation(arg0);
                }
            });
    }

    /// <summary>
    /// Transition from the current state via the specified trigger.
    /// The target state is determined by the configuration of the current state.
    /// Actions associated with leaving the current state and entering the new one
    /// will be invoked.
    /// </summary>
    /// <param name="trigger">The trigger to fire.</param>
    /// <exception cref="System.InvalidOperationException">The current state does
    /// not allow the trigger to be fired.</exception>
    public void fire(TTrigger trigger) throws UnhandledTriggerException, NoTransitionsPermittedException
    {
        publicFire(trigger, new Object[0]);
    }

    /// <summary>
    /// Transition from the current state via the specified trigger.
    /// The target state is determined by the configuration of the current state.
    /// Actions associated with leaving the current state and entering the new one
    /// will be invoked.
    /// </summary>
    /// <typeparam name="TArg0">Type of the first trigger argument.</typeparam>
    /// <param name="trigger">The trigger to fire.</param>
    /// <param name="arg0">The first argument.</param>
    /// <exception cref="System.InvalidOperationException">The current state does
    /// not allow the trigger to be fired.</exception>
    public <TArg0> void fire(TriggerWithParameters1<TArg0, TState, TTrigger> trigger, TArg0 arg0)
        throws UnhandledTriggerException, NoTransitionsPermittedException
    {
        ArgumentValidator.argumentNotNull(trigger, "trigger");
        publicFire(trigger.getTrigger(), arg0);
    }

    /// <summary>
    /// Transition from the current state via the specified trigger.
    /// The target state is determined by the configuration of the current state.
    /// Actions associated with leaving the current state and entering the new one
    /// will be invoked.
    /// </summary>
    /// <typeparam name="TArg0">Type of the first trigger argument.</typeparam>
    /// <typeparam name="TArg1">Type of the second trigger argument.</typeparam>
    /// <param name="arg0">The first argument.</param>
    /// <param name="arg1">The second argument.</param>
    /// <param name="trigger">The trigger to fire.</param>
    /// <exception cref="System.InvalidOperationException">The current state does
    /// not allow the trigger to be fired.</exception>
    public <TArg0, TArg1> void fire(TriggerWithParameters2<TArg0, TArg1, TState, TTrigger> trigger,
        TArg0 arg0, TArg1 arg1) throws UnhandledTriggerException, NoTransitionsPermittedException
    {
        ArgumentValidator.argumentNotNull(trigger, "trigger");
        publicFire(trigger.getTrigger(), arg0, arg1);
    }

    /// <summary>
    /// Transition from the current state via the specified trigger.
    /// The target state is determined by the configuration of the current state.
    /// Actions associated with leaving the current state and entering the new one
    /// will be invoked.
    /// </summary>
    /// <typeparam name="TArg0">Type of the first trigger argument.</typeparam>
    /// <typeparam name="TArg1">Type of the second trigger argument.</typeparam>
    /// <typeparam name="TArg2">Type of the third trigger argument.</typeparam>
    /// <param name="arg0">The first argument.</param>
    /// <param name="arg1">The second argument.</param>
    /// <param name="arg2">The third argument.</param>
    /// <param name="trigger">The trigger to fire.</param>
    /// <exception cref="System.InvalidOperationException">The current state does
    /// not allow the trigger to be fired.</exception>
    public <TArg0, TArg1, TArg2> void fire(
        TriggerWithParameters3<TArg0, TArg1, TArg2, TState, TTrigger> trigger, TArg0 arg0, TArg1 arg1,
        TArg2 arg2) throws UnhandledTriggerException, NoTransitionsPermittedException
    {
        ArgumentValidator.argumentNotNull(trigger, "trigger");
        publicFire(trigger.getTrigger(), arg0, arg1, arg2);
    }

    @SuppressWarnings("unchecked")
    private void publicFire(TTrigger trigger, Object... args) throws UnhandledTriggerException,
        NoTransitionsPermittedException
    {
        TriggerWithParameters<TState, TTrigger> configuration;
        if (triggerConfiguration.containsKey(trigger))
        {
            configuration = triggerConfiguration.get(trigger);
            configuration.validateParameters(args);
        }

        TriggerBehaviour<TState, TTrigger> triggerBehaviour;
        try
        {
            triggerBehaviour = getCurrentRepresentation().tryFindHandler(trigger);
        }
        catch (Exception e)
        {
            if (unhandledTriggerRunnable != null)
            {
                if (unhandledTriggerRunnable instanceof UnhandledTransitionRunnable)
                {
                    ((UnhandledTransitionRunnable<TState, TTrigger>) unhandledTriggerRunnable).setState(getCurrentRepresentation().getUnderlyingState());
                    ((UnhandledTransitionRunnable<TState, TTrigger>) unhandledTriggerRunnable).setTrigger(trigger);
                }
                unhandledTriggerRunnable.run();

            }
            else
            {
                throw new NoTransitionsPermittedException(trigger.toString() + " "
                    + getCurrentRepresentation().getUnderlyingState());
            }
            return;
        }

        TState source = getState();
        TState destination;
        try
        {
            destination = triggerBehaviour.resultsInTransitionFrom(source, args);
            Transition<TState, TTrigger> transition = new Transition<TState, TTrigger>(source, destination,
                trigger);

            getCurrentRepresentation().exit(transition);
            setState(transition.getDestination());
            getCurrentRepresentation().enter(transition, args);

        }
        catch (Exception e)
        {

        }
    }

    /**
     * Override the default behaviour of throwing an exception when an unhandled trigger is fired.
     * @param unhandledTriggerAction An action to call when an unhandled trigger is fired.  Can be {@link Runnable} or {@link UnhandledTransitionRunnable}.
     */
    public void onUnhandledTrigger(Runnable unhandledTriggerAction)
    {
        if (unhandledTriggerAction == null)
        {
            throw new IllegalArgumentException("unhandledTriggerAction");
        }

        this.unhandledTriggerRunnable = unhandledTriggerAction;
    }

    /// <summary>
    /// Determine if the state machine is in the supplied state.
    /// </summary>
    /// <param name="state">The state to test for.</param>
    /// <returns>True if the current state is equal to, or a substate of,
    /// the supplied state.</returns>
    public Boolean inState(TState state)
    {
        return getCurrentRepresentation().includedIn(state);
    }

    /// <summary>
    /// Returns true if <paramref name="trigger"/> can be fired
    /// in the current state.
    /// </summary>
    /// <param name="trigger">Trigger to test.</param>
    /// <returns>True if the trigger can be fired, false otherwise.</returns>
    public Boolean canFire(TTrigger trigger)
    {
        return getCurrentRepresentation().canHandle(trigger);
    }

    /// <summary>
    /// A human-readable representation of the state machine.
    /// </summary>
    /// <returns>A description of the current state and permitted triggers.</returns>
    public String toString()
    {
        List<TTrigger> permittedTriggers = getPermittedTriggers();
        List<String> parameters = new ArrayList<String>();

        for (TTrigger tTrigger : permittedTriggers)
        {
            parameters.add(tTrigger.toString());
        }

        return String.format("StateMachine {{ State = {0}, PermittedTriggers = {{ {1} }}}}", getState(),
            StringUtils.join(parameters, ", "));
    }

    /// <summary>
    /// Specify the arguments that must be supplied when a specific trigger is fired.
    /// </summary>
    /// <typeparam name="TArg0">Type of the first trigger argument.</typeparam>
    /// <param name="trigger">The underlying trigger value.</param>
    /// <returns>An object that can be passed to the Fire() method in order to 
    /// fire the parameterised trigger.</returns>
    public <TArg0> TriggerWithParameters1<TArg0, TState, TTrigger> setTriggerParameters(TTrigger trigger,
        Class<TArg0> classe0) throws CannotReconfigureParametersException
    {
        TriggerWithParameters1<TArg0, TState, TTrigger> configuration = new TriggerWithParameters1<TArg0, TState, TTrigger>(
            trigger, classe0);
        saveTriggerConfiguration(configuration);
        return configuration;
    }

    /// <summary>
    /// Specify the arguments that must be supplied when a specific trigger is fired.
    /// </summary>
    /// <typeparam name="TArg0">Type of the first trigger argument.</typeparam>
    /// <typeparam name="TArg1">Type of the second trigger argument.</typeparam>
    /// <param name="trigger">The underlying trigger value.</param>
    /// <returns>An object that can be passed to the Fire() method in order to 
    /// fire the parameterised trigger.</returns>
    public <TArg0, TArg1> TriggerWithParameters2<TArg0, TArg1, TState, TTrigger> setTriggerParameters(
        TTrigger trigger, Class<TArg0> classe0, Class<TArg1> classe1)
        throws CannotReconfigureParametersException
    {
        TriggerWithParameters2<TArg0, TArg1, TState, TTrigger> configuration = new TriggerWithParameters2<TArg0, TArg1, TState, TTrigger>(
            trigger, classe0, classe1);
        saveTriggerConfiguration(configuration);
        return configuration;
    }

    /// <summary>
    /// Specify the arguments that must be supplied when a specific trigger is fired.
    /// </summary>
    /// <typeparam name="TArg0">Type of the first trigger argument.</typeparam>
    /// <typeparam name="TArg1">Type of the second trigger argument.</typeparam>
    /// <typeparam name="TArg2">Type of the third trigger argument.</typeparam>
    /// <param name="trigger">The underlying trigger value.</param>
    /// <returns>An object that can be passed to the Fire() method in order to 
    /// fire the parameterised trigger.</returns>
    public <TArg0, TArg1, TArg2> TriggerWithParameters3<TArg0, TArg1, TArg2, TState, TTrigger> setTriggerParameters(
        TTrigger trigger, Class<TArg0> classe0, Class<TArg1> classe1, Class<TArg2> classe2) throws Exception
    {
        TriggerWithParameters3<TArg0, TArg1, TArg2, TState, TTrigger> configuration = new TriggerWithParameters3<TArg0, TArg1, TArg2, TState, TTrigger>(
            trigger, classe0, classe1, classe2);
        saveTriggerConfiguration(configuration);
        return configuration;
    }

    /**
     * @param trigger
     * @throws CannotReconfigureParametersException
     */
    private void saveTriggerConfiguration(TriggerWithParameters<TState, TTrigger> trigger)
        throws CannotReconfigureParametersException
    {
        if (triggerConfiguration.containsKey(trigger.getTrigger()))
            throw new CannotReconfigureParametersException(trigger.toString());

        triggerConfiguration.put(trigger.getTrigger(), trigger);
    }

    /**
     * @param dotFile
     * @throws UnsupportedEncodingException
     */
    public void generateDotFileInto(OutputStream dotFile) throws UnsupportedEncodingException
    {
        OutputStreamWriter w = new OutputStreamWriter(dotFile, "UTF-8");
        PrintWriter writer = new PrintWriter(w);
        writer.write("digraph G {\n");
        for (Entry<TState, StateRepresentation<TState, TTrigger>> entry : this.stateConfiguration.entrySet())
        {
            Map<TTrigger, List<TriggerBehaviour<TState, TTrigger>>> behaviours = entry.getValue().getTriggerBehaviours();
            for (Entry<TTrigger, List<TriggerBehaviour<TState, TTrigger>>> behaviour : behaviours.entrySet())
            {
                for (TriggerBehaviour<TState, TTrigger> triggerBehaviour : behaviour.getValue())
                {
                    if (triggerBehaviour instanceof TransitioningTriggerBehaviour)
                    {
                        try
                        {
                            writer.write(String.format("\t%s -> %s;\n", entry.getKey(),
                                triggerBehaviour.resultsInTransitionFrom(null)));
                        }
                        catch (Exception e)
                        {
                            //Intentionally ignored.
                        }
                    }
                }
            }
        }
        writer.write("}");
        writer.close();
    }
}
